package roborally.supporting;

import java.util.HashSet;
import java.util.Set;

import roborally.factory.PositionFactory;

import exception.IllegalPositionException;
import exception.OverFlowException;
import be.kuleuven.cs.som.annotate.*;

/**
 * A class representing a position in a two dimensional space.
 * 
 * @invar 
 * 		| isValidX(this.getX()) && isValidY(this.getY())
 * 
 * @author Molderez Tom, 2de bach Ingenieurswetenschappen Computerwetenschappen-Elektrotechniek and Reniers Jorn, 2de bach Ingenieurswetenschappen Werktuigkunde-Computerwetenschappen
 * @version 2.0
 */
@Value
public class Position {
	
	/**
	 * Creates a new position for the given coordinates.
	 * 
	 * @param x The x-coordinate of the new Position.
	 * @param y The y-coordinate of the new Position.
	 * @post 
	 * 		| (new this).getX()==x
	 * @post 
	 * 		| (new this).getY()==y
	 * @throws IllegalPositionException
	 * 		| !isValidX(x) || !isValidY(y)
	 * @note This constructor may not be used. To create a position properly and efficiently without
	 * spending resources, use the PositionFactory class. 
	 */
	public Position(long x, long y) throws IllegalPositionException {
		if(isValidX(x) && isValidY(y)) {
			this.x=x;
			this.y=y;
		}
		else
			throw new IllegalPositionException(x,y, "The position that should be made is invalid.");
	}
	
	/**
	 * The origin, located at (0,0).
	 */
	public static final Position ORIGIN = PositionFactory.get().create(0,0);
	
	/**
	 * Controls whether the x-coordinate is valid.
	 * 
	 * @param x The x-coordinate to be checked.
	 * @return 
	 * 		| result == x>=0;
	 */
	@Raw
	public boolean isValidX(long x) {
		return x >= 0;
	}
	
	/**
	 * Returns the x-coordinate of this position.
	 */
	@Basic @Immutable @Raw
	public long getX(){
		return this.x;
	}
	
	private final long x;
	
	/**
	 * Controls whether the y-coordinate is valid.
	 * 
	 * @param y The y-coordinate to be checked.
	 * @return 
	 * 		| result == y>=0;
	 */
	@Raw
	public boolean isValidY(long y) {
		return y >= 0;
	}
	
	/**
	 * Returns the y-coordinate of this position.
	 */
	@Basic @Immutable @Raw
	public long getY(){
		return this.y;
	}
	
	private final long y;
	
	/**
	 * Checks whether this position and the other are the same, and being the same means having the same x- and y-coordinate.
	 * 
	 * @param object The object to compare this position to.
	 * @return 
	 * 		| if object == null 
	 * 		|	then result == false
	 * 		| if Position.class == object.getClass()
	 * 		|	then result == Position.class.cast(object).getX() == this.getX() && Position.class.cast(object).getY() == this.getY() 
	 * 		| else
	 * 		| 	result == false 
	 */
	@Override
	public boolean equals(Object object) {
		if (object == null) 
			return false;
		if(Position.class == object.getClass() ) 
			return Position.class.cast(object).getX() == this.getX() && Position.class.cast(object).getY() == this.getY();
		else
			return false;
	}
	
	/**
	 * Returns a textual representation of this.
	 * 
	 * @return A textual representation of this.
	 * 		| result == this.getClass().getSimpleName() + "(" + this.getX() + "," + this.getY() + ")"
	 */
	@Override
	public String toString() {
		return this.getClass().getSimpleName() + "(" + this.getX() + ", " + this.getY() + ")";
	}
	
	/**
	 * Throws an exception whenever a method tries to clone a given position.
	 * @throws CloneNotSupportedException
	 * 		| true
	 */
	@Override
	public Position clone() throws CloneNotSupportedException {
		throw new CloneNotSupportedException("Position objects can not be cloned.");
	}
	
	/**
	 * Returns a hashcode of this object according to the Java API specifications.
	 * 
	 * @return 
	 * 		| result == (int) ( this.getX()+Integer.MAX_VALUE/2*this.getY() )
	 */
	@Override
	public int hashCode(){
		return (int) ( this.getX()+Integer.MAX_VALUE/2*this.getY() );
	}
	
	/**
	 * Controls whether this position and the other are next to each other.
	 * 
	 * @param position The position to compare this position to.
	 * @return 	
	 * 		| result ==  1==  this.getManhattanDistance(position) )
	 * @throws IllegalArgumentException
	 * 		| position == null
	 */
	public boolean nextTo(Position position) throws IllegalArgumentException{
		if(position == null)
			throw new IllegalArgumentException("position can't be next to null");
		return (1 == this.getManhattanDistance(position));
	}
	
	/**
	 * Controls whether this position and the other are on one line.
	 * 
	 * @param position The position to compare this position to.
	 * @return 
	 * 		| result == this.getX()==position.getX() || this.getY() == position.getY()
	 * @throws IllegalArgumentException
	 * 		| position == null
	 */
	public boolean onOneLine(Position position) throws IllegalArgumentException{
		if(position == null)
			throw new IllegalArgumentException("position can't be on one line with null");
		return this.getX()==position.getX() || this.getY() == position.getY();
	}
	
	/**
	 * Computes the Manhattan distance between this position and the other.
	 * 
	 * @param position The other position which will be used to compute the Manhattan distance.
	 * @return
	 * 		| if ( Math.abs(this.getX() - position.getX()) + Math.abs(this.getY() - position.getY()) >= 0)
	 * 		|	result ==  Math.abs(this.getX() - position.getX()) + Math.abs(this.getY() - position.getY())
	 * @throws IllegalArgumentException
	 * 		| position == null
	 * @throws OverFlowException
	 * 		| if ( Math.abs(this.getX() - position.getX()) + Math.abs(this.getY() - position.getY()) <0)
	 */
	public long getManhattanDistance(Position position) throws OverFlowException,IllegalArgumentException {
		if(position == null)
			throw new IllegalArgumentException("cannot calculate the distance to null");
		long result = Math.abs(this.getX() - position.getX()) + Math.abs(this.getY() - position.getY());
		if(result < 0) // the difference in x is at most Long.MAX_VALUE, just like the difference in y. So if overflow would occur, the sum is always negative.
			throw new OverFlowException(Long.class,"more than long.MAX_VALUE");
		else
			return result;
	}
	
	/**
	 * Returns the position of an object located at this position if it moves in the direction of orientation.
	 * 
	 * @param orientation The direction in which the object at this position must move.
	 * @return 
	 * 		|if (orientation == Orientation.UP)
	 * 		| 	return PositionFactory.getInstance().newPosition(this.getX(), this.getY()-1);
	 * 		| else if (orientation == Orientation.RIGHT)
	 * 		| 	return PositionFactory.getInstance().newPosition(this.getX()+1, this.getY());
	 * 		| else if (orientation == Orientation.DOWN)
	 * 		| 	return PositionFactory.getInstance().newPosition(this.getX(), this.getY()+1);
	 * 		| else
	 * 		| 	return PositionFactory.getInstance().newPosition(this.getX()-1, this.getY());
	 * @throws IllegalArgumentException
	 * 		| orientation == null
	 * @throws IllegalPositionException
	 * 		| ( (orientation == Orientation.UP && !isValidY(this.getY()-1))
	 * 		|	|| (orientation == Orientation.LEFT && !isValidX(this.getX()-1))
	 * 		|	|| (orientation == Orientation.DOWN && !isValidY(this.getY()+1))
	 * 		|	|| (orientation == Orientation.RIGHT && !isValidX(this.getX()+1)) )
	 */
	public Position move(Orientation orientation) throws IllegalArgumentException,IllegalPositionException {
		if (orientation == null)
			throw new IllegalArgumentException("orientation can't be null");
		if (orientation == Orientation.UP)
			return PositionFactory.get().create(this.getX(), this.getY()-1);
		else if (orientation == Orientation.RIGHT)
			return PositionFactory.get().create(this.getX()+1, this.getY());
		else if (orientation == Orientation.DOWN)
			return PositionFactory.get().create(this.getX(), this.getY()+1);
		else
			return PositionFactory.get().create(this.getX()-1, this.getY());
	}
	
	/**
	 * Returns the legal neighbors of this position with legal meaning that the position satisfies its classinvariants.
	 * 
	 * @return 
	 * 		| result = new Set<Position>
	 * 		| if(isValidY(this.getY()-1))
	 * 		|	result.add(move(Orientation.UP));
	 * 		| if(isValidX(this.getX()+1))
	 * 		|	result.add(move(Orientation.RIGHT));
	 * 		| if(isValidY(this.getY()+1))
	 * 		|	result.add(move(Orientation.DOWN));
	 * 		| if(isValidX(this.getX()-1))
	 * 		|	result.add(move(Orientation.LEFT));
	 */
	public Set<Position> getLegalNeighbors(){
		Set<Position> result = new HashSet<Position>();
		if(isValidY(this.getY()-1))
			result.add(move(Orientation.UP));
		if(isValidX(this.getX()+1))
			result.add(move(Orientation.RIGHT));
		if(isValidY(this.getY()+1))
			result.add(move(Orientation.DOWN));
		if(isValidX(this.getX()-1))
			result.add(move(Orientation.LEFT));
		return result;
	}
}
